Passed
Pull Request — master (#15)
by
unknown
02:48
created

LevelStorage.reset   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
1
import { DependencyLink, DependencyNode } from '../../components/types';
2
import { select, Selection, event, BaseType, selectAll } from 'd3-selection';
3
import { Simulation } from 'd3-force';
4
import { drag } from 'd3-drag';
5
import { zoom, zoomIdentity } from 'd3-zoom';
6
7
export type NodeSelection<T extends BaseType> = Selection<T, DependencyNode, Element, HTMLElement>;
8
9
export type LinkSelection = Selection<SVGPathElement, DependencyLink, SVGGElement, DependencyNode>;
10
11
export enum LabelColors {
12
    PROVIDER = '#00BFC2',
13
    CONSUMER = '#039881',
14
    PROVIDER_CONSUMER = '#03939F',
15
    DEFAULT = '#dcdee0',
16
}
17
18
export enum TextColors {
19
    HIGHLIGHTED = 'WHITE',
20
    DEFAULT = '#5E6063',
21
}
22
23
export function getLabelTextDimensions(node: Node) {
24
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
25
26
    if (!textNode) {
27
        return undefined;
28
    }
29
30
    return textNode.getBBox();
31
}
32
33
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
34
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
35
        .selectAll<SVGGElement, DependencyNode>('g')
36
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
37
        .node();
38
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
39
}
40
41
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
42
    return (
43
        Math.max(
44
            ...labelNodesGroup
45
                .selectAll<HTMLElement, DependencyNode>('g')
46
                .filter((node: DependencyNode) => node.level > 0)
47
                .data()
48
                .map((node: DependencyNode) => node.level)
49
        ) - 1
50
    );
51
}
52
53
export function highlight(clickedNode: DependencyNode, links: LinkSelection) {
54
    const linksData = links.data();
55
    const labelNodes = selectAllNodes();
56
57
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
58
59
    if (visitedNodes.length === 1) {
60
        return;
61
    }
62
63
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
64
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
65
        const labelElement = this.firstElementChild;
66
        const textElement = this.lastElementChild;
67
68
        if (!labelElement || !textElement) {
69
            return;
70
        }
71
72
        if (areNodesDirectlyConnected) {
73
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
74
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
75
        } else {
76
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
77
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
78
        }
79
    });
80
}
81
82
export function selectHighLightedNodes() {
83
    return selectAllNodes().filter(function(this: SVGGElement) {
84
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== LabelColors.DEFAULT : false;
85
    });
86
}
87
88
export function selectAllNodes() {
89
    return select('#labels').selectAll<SVGGElement, DependencyNode>('g');
90
}
91
92
export function selectHighlightBackground() {
93
    return select('#highlight-background');
94
}
95
96
function selectDetailsButtonWrapper() {
97
    return select('#details-button');
98
}
99
100
export function selectDetailsButtonRect() {
101
    return selectDetailsButtonWrapper().select('rect');
102
}
103
104
export function selectDetailsButtonText() {
105
    return selectDetailsButtonWrapper().select('text');
106
}
107
108
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
109
    if (dimension) {
110
        const svgContainer = select('#container');
111
112
        const width = Number(svgContainer.attr('width'));
113
        const height = Number(svgContainer.attr('height'));
114
115
        const scaleValue = scale || Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
116
117
        svgContainer
118
            .attr('data-scale', scaleValue)
119
            .transition()
120
            .duration(750)
121
            .call(
122
                zoom<any, any>().on('zoom', zoomed).transform,
123
                zoomIdentity
124
                    .translate(width / 2, height / 2)
125
                    .scale(scaleValue)
126
                    .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
127
            );
128
    }
129
}
130
131
function getScaleValue() {
132
    return select('#container').attr('data-scale');
133
}
134
135
function removeHighlightBackground() {
136
    const detailsButtonRectSelection = selectDetailsButtonRect();
137
    const detailsButtonTextSelection = selectDetailsButtonText();
138
    selectAll([selectHighlightBackground().node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
139
        .transition()
140
        .duration(750)
141
        .style('opacity', 0);
142
}
143
144
function showHighlightBackground(dimension: ReturnType<typeof findGroupBackgroundDimension>) {
145
    if (dimension) {
146
        const highlightBackground = selectHighlightBackground();
147
        const detailsButtonRectSelection = selectDetailsButtonRect();
148
        const detailsButtonTextSelection = selectDetailsButtonText();
149
150
        const scaleValue = Number(getScaleValue());
151
152
        const isBackgroundNotActive = highlightBackground.style('opacity') !== '0.05';
153
154
        const buttonWidth = 100 * (1 / scaleValue);
155
        const buttonHeight = 40 * (1 / scaleValue);
156
        const buttonMargin = 20 * (1 / scaleValue);
157
        const buttonX = dimension.x + dimension.width - buttonWidth - buttonMargin;
158
        const buttonY = dimension.y + dimension.height - buttonHeight - buttonMargin;
159
        const buttonTextFontSize = 20 * (1 / scaleValue);
160
        const buttonTextPositionX = dimension.x + dimension.width - buttonWidth / 2 - buttonMargin;
161
        const buttonTextPositionY = dimension.y + dimension.height - buttonHeight / 2 + 7 * (1 / scaleValue) - buttonMargin;
162
163
        if (isBackgroundNotActive) {
164
            highlightBackground
165
                .attr('x', dimension.x)
166
                .attr('y', dimension.y)
167
                .attr('width', dimension.width)
168
                .attr('height', dimension.height)
169
                .transition()
170
                .duration(750)
171
                .style('opacity', 0.05);
172
173
            detailsButtonRectSelection
174
                .attr('width', buttonWidth)
175
                .attr('height', buttonHeight)
176
                .attr('x', buttonX)
177
                .attr('y', buttonY)
178
                .transition()
179
                .duration(750)
180
                .style('opacity', 1);
181
182
            detailsButtonTextSelection
183
                .attr('font-size', buttonTextFontSize)
184
                .style('text-anchor', 'middle')
185
                .attr('x', buttonTextPositionX)
186
                .attr('y', buttonTextPositionY)
187
                .transition()
188
                .duration(750)
189
                .style('opacity', 1);
190
        } else {
191
            const elementsNextAttributes = [
192
                {
193
                    x: dimension.x,
194
                    y: dimension.y,
195
                    width: dimension.width,
196
                    height: dimension.height,
197
                },
198
                {
199
                    x: buttonX,
200
                    y: buttonY,
201
                    width: buttonWidth,
202
                    height: buttonHeight,
203
                },
204
                {
205
                    fontSize: buttonTextFontSize,
206
                    x: buttonTextPositionX,
207
                    y: buttonTextPositionY,
208
                },
209
            ];
210
211
            selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
212
                .data(elementsNextAttributes)
213
                .transition()
214
                .duration(750)
215
                .attr('x', node => node.x)
216
                .attr('y', node => node.y)
217
                .attr('width', node => node.width || 0)
218
                .attr('height', node => node.height || 0)
219
                .attr('font-size', node => node.fontSize || 0);
220
        }
221
    }
222
}
223
224
export function zoomToHighLightedNodes() {
225
    const highlightedNodes = selectHighLightedNodes();
226
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
227
228
    centerScreenToDimension(dimension);
229
    showHighlightBackground(dimension);
230
}
231
232
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
233
    nodes.forEach((node: DependencyNode) => (node.level = 0));
234
235
    const visitedNodes: DependencyNode[] = [];
236
    const nodesToVisit: DependencyNode[] = [];
237
238
    nodesToVisit.push({ ...clickedNode, level: 1 });
239
240
    while (nodesToVisit.length > 0) {
241
        const currentNode = nodesToVisit.shift();
242
243
        if (!currentNode) {
244
            return [];
245
        }
246
247
        currentNode.links.forEach((node: DependencyNode) => {
248
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
249
                node.level = currentNode.level + 1;
250
                nodesToVisit.push(node);
251
            }
252
        });
253
254
        visitedNodes.push(currentNode);
255
    }
256
257
    return visitedNodes;
258
}
259
260
function containsNode(arr: DependencyNode[], node: DependencyNode) {
261
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
262
}
263
264
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
265
    node1: T,
266
    node2: K
267
): Boolean {
268
    return node1.name === node2.name && node1.version === node2.version;
269
}
270
271
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
272
    return (
273
        a.index === b.index ||
274
        links.some(
275
            link =>
276
                (link.source.index === a.index && link.target.index === b.index) ||
277
                (link.source.index === b.index && link.target.index === a.index)
278
        )
279
    );
280
}
281
282
export function getHighLightedLabelColor(node: DependencyNode) {
283
    const { isConsumer, isProvider } = node;
284
285
    if (isConsumer && isProvider) {
286
        return LabelColors.PROVIDER_CONSUMER;
287
    }
288
289
    if (isProvider) {
290
        return LabelColors.PROVIDER;
291
    }
292
293
    if (isConsumer) {
294
        return LabelColors.CONSUMER;
295
    }
296
297
    return LabelColors.DEFAULT;
298
}
299
300
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
301
    return drag<SVGGElement, DependencyNode>()
302
        .on('start', (node: DependencyNode) => {
303
            if (!selectHighLightedNodes().data().length) {
304
                dragStarted(node, simulation);
305
            }
306
        })
307
        .on('drag', (node: DependencyNode) => {
308
            if (!selectHighLightedNodes().data().length) {
309
                dragged(node);
310
            }
311
        })
312
        .on('end', (node: DependencyNode) => {
313
            if (!selectHighLightedNodes().data().length) {
314
                dragEnded(node, simulation);
315
            }
316
        });
317
}
318
319
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
320
    if (!event.active) {
321
        simulation.alphaTarget(0.3).restart();
322
    }
323
    node.fx = node.x;
324
    node.fy = node.y;
325
}
326
327
function dragged(node: DependencyNode) {
328
    node.fx = event.x;
329
    node.fy = event.y;
330
}
331
332
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
333
    if (!event.active) {
334
        simulation.alphaTarget(0);
335
    }
336
    node.fx = null;
337
    node.fy = null;
338
}
339
340
export function zoomed() {
341
    const { transform } = event;
342
    const zoomLayer = select('#zoom');
343
    zoomLayer.attr('transform', transform);
344
    zoomLayer.attr('stroke-width', 1 / transform.k);
345
}
346
347
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
348
    if (nodesGroup.length === 0) {
349
        return undefined;
350
    }
351
352
    let upperLimitNode = nodesGroup[0];
353
    let lowerLimitNode = nodesGroup[0];
354
    let leftLimitNode = nodesGroup[0];
355
    let rightLimitNode = nodesGroup[0];
356
357
    nodesGroup.forEach((node: DependencyNode) => {
358
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
359
            return;
360
        }
361
        if (node.x > rightLimitNode.x) {
362
            rightLimitNode = node;
363
        }
364
365
        if (node.x < leftLimitNode.x) {
366
            leftLimitNode = node;
367
        }
368
369
        if (node.y < upperLimitNode.y) {
370
            upperLimitNode = node;
371
        }
372
373
        if (node.y > lowerLimitNode.y) {
374
            lowerLimitNode = node;
375
        }
376
    });
377
378
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
379
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
380
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
381
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
382
383
    return {
384
        x: leftLimitWithOffset,
385
        y: upperLimitWithOffset,
386
        width,
387
        height,
388
    };
389
}
390
391
export function setResetViewHandler() {
392
    LevelStorage.reset();
393
    const svgContainer = select('#container');
394
    svgContainer.on('click', () => {
395
        const highlightedNodes = selectHighLightedNodes();
396
        if (highlightedNodes.data().length) {
397
            selectAllNodes().each((node: DependencyNode) => (node.level = 0));
398
399
            const dimension = findGroupBackgroundDimension(highlightedNodes.data());
400
401
            highlightedNodes.each(function(this: SVGGElement) {
402
                const labelElement = this.firstElementChild;
403
                const textElement = this.lastElementChild;
404
405
                if (!labelElement || !textElement) {
406
                    return;
407
                }
408
409
                select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
410
                select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
411
            });
412
413
            removeHighlightBackground();
414
415
            centerScreenToDimension(dimension, 1);
416
        }
417
    });
418
}
419
420
export class LevelStorage {
421
    private static level: number = 1;
422
    private static maxLevel: number = 1;
423
424
    public static getLevel(): number {
425
        return this.level;
426
    }
427
428
    public static increase() {
429
        this.level = this.level + 1;
430
    }
431
432
    public static decrease() {
433
        this.level = this.level - 1;
434
    }
435
436
    public static isBelowMax() {
437
        return this.level < this.maxLevel;
438
    }
439
440
    static isAboveMin() {
441
        return this.level > 1;
442
    }
443
444
    static setMaxLevel(maxLevel: number) {
445
        this.maxLevel = maxLevel;
446
    }
447
448
    static getMaxLevel(): number {
449
        return this.maxLevel;
450
    }
451
452
    public static reset() {
453
        this.level = 1;
454
        this.maxLevel = 1;
455
    }
456
}
457